﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Nwc.XmlRpc;
using ExtensionLoader;
using ExtensionLoader.Config;
using HttpServer;
using OpenMetaverse;
using OpenMetaverse.Http;
using OpenMetaverse.StructuredData;
using CableBeachMessages;

namespace WorldServer
{
    public static class LindenLoginHelper
    {
        public const int REQUEST_TIMEOUT = 1000 * 100;

        static Random rng = new Random();

        public static XmlRpcResponse LoginMethod(WorldServer server, XmlRpcRequest request, IHttpRequest httpRequest, Avatar avatar, UUID sessionID, string welcomeMessage)
        {
            Hashtable requestData = (Hashtable)request.Params[0];
            
            string startLocation = (requestData.ContainsKey("start") ? (string)requestData["start"] : "last");
            string version = (requestData.ContainsKey("version") ? (string)requestData["version"] : "Unknown");

            Logger.InfoFormat("[LindenLoginHelper] Received XML-RPC login request for {0} with client \"{1}\" to destination \"{2}\"",
                avatar.Identity, version, startLocation);

            // Construct the login response
            LindenLoginData response = new LindenLoginData();

            //response.ActiveGestures
            response.AgentID = avatar.ID;
            response.BuddyList = GetBuddyList(avatar.ID);
            SetClassifiedCategories(ref response);
            response.FirstName = avatar.GetAttribute(AvatarAttributes.FIRST_NAME).AsString();
            response.HomeLookAt = avatar.GetAttribute(AvatarAttributes.HOME_LOOKAT).AsVector3();
            response.HomePosition = avatar.GetAttribute(AvatarAttributes.HOME_POSITION).AsVector3();
            response.HomeRegionX = avatar.GetAttribute(AvatarAttributes.HOME_REGION_X).AsUInteger();
            response.HomeRegionY = avatar.GetAttribute(AvatarAttributes.HOME_REGION_Y).AsUInteger();
            response.LastName = avatar.GetAttribute(AvatarAttributes.LAST_NAME).AsString();
            response.Login = true;
            response.Message = welcomeMessage;
            response.SessionID = sessionID;
            response.SecureSessionID = UUID.Random();

            RegionInfo startRegion;
            Vector3 startPosition;
            // Find the simulator this avatar will start in
            if (TryGetStartingRegion(server, avatar, startLocation, ref response, out startRegion, out startPosition))
            {
                Logger.Debug("[LindenLoginHelper] Starting region located for " + avatar.Identity);

                // If the avatar doesn't have a home region, set it to the current region
                if (response.HomeRegionX == 0 && response.HomeRegionY == 0)
                {
                    response.HomeRegionX = response.RegionX;
                    response.HomeRegionY = response.RegionY;
                }

                // Contact the inventory server to get a skeleton listing of inventory folders and
                // information about what the avatar is currently wearing
                if (TryGetInventoryInfo(server, avatar, ref response))
                {
                    Logger.Debug("[LindenLoginHelper] Inventory info retrieved for " + avatar.Identity);

                    // Contact the starting simulator to authorize this login
                    if (TryPrepareLogin(server, avatar, startRegion, startPosition, version, httpRequest.RemoteEndPoint.Address, ref response))
                    {
                        Logger.Debug("[LindenLoginHelper] Login to " + startRegion.Name + " prepared for " + avatar.Identity + ", returning response");
                        return response.ToXmlRpcResponse();
                    }
                }
            }
            else
            {
                return CreateLoginNoRegionResponse();
            }

            return CreateLoginInternalErrorResponse();
        }

        public static void SetClassifiedCategories(ref LindenLoginData response)
        {
            response.AddClassifiedCategory(1, "Shopping");
            response.AddClassifiedCategory(2, "Land Rental");
            response.AddClassifiedCategory(3, "Property Rental");
            response.AddClassifiedCategory(4, "Special Attraction");
            response.AddClassifiedCategory(5, "New Products");
            response.AddClassifiedCategory(6, "Employment");
            response.AddClassifiedCategory(7, "Wanted");
            response.AddClassifiedCategory(8, "Service");
            response.AddClassifiedCategory(9, "Personal");
        }

        public static Hashtable GetBuddyList(UUID avatarID)
        {
            // TODO: Buddy list support
            return new Hashtable(0);
        }

        public static bool TryGetStartingRegion(WorldServer server, Avatar avatar, string startLocation, ref LindenLoginData response,
            out RegionInfo startRegion, out Vector3 startPosition)
        {
            startPosition = Vector3.Zero;
            startRegion = new RegionInfo();

            uint regionX, regionY;

            switch (startLocation)
            {
                case "home":
                case "safe":
                    // Try and get the home location for this avatar
                    regionX = avatar.GetAttribute(AvatarAttributes.HOME_REGION_X).AsUInteger();
                    regionY = avatar.GetAttribute(AvatarAttributes.HOME_REGION_Y).AsUInteger();
                    Logger.Debug("Trying to fetch home region at " + regionX + "," + regionY);
                    startRegion = GetNearestRegion(server, avatar, regionX, regionY);
                    if (startRegion.ID != UUID.Zero)
                    {
                        startPosition = startRegion.DefaultPosition;
                        response.StartLocation = "home";
                    }

                    break;
                case "last":
                    // Try and get the last location for this avatar
                    regionX = avatar.GetAttribute(AvatarAttributes.LAST_REGION_X).AsUInteger();
                    regionY = avatar.GetAttribute(AvatarAttributes.LAST_REGION_Y).AsUInteger();
                    Logger.Debug("Trying to fetch last region at " + regionX + "," + regionY);
                    startRegion = GetNearestRegion(server, avatar, regionX, regionY);
                    if (startRegion.ID != UUID.Zero)
                    {
                        startPosition = startRegion.DefaultPosition;
                        response.StartLocation = "last";
                    }

                    break;
                default:
                    Regex reURI = new Regex(@"^uri:(?<region>[^&]+)&(?<x>\d+)&(?<y>\d+)&(?<z>\d+)$");
                    Match uriMatch = reURI.Match(startLocation);
                    if (uriMatch != null && server.MapProvider.TryFetchRegion(uriMatch.Groups["region"].Value, out startRegion) == BackendResponse.Success)
                    {
                        Single.TryParse(uriMatch.Groups["x"].Value, out startPosition.X);
                        Single.TryParse(uriMatch.Groups["y"].Value, out startPosition.Y);
                        Single.TryParse(uriMatch.Groups["z"].Value, out startPosition.Z);
                    }
                    else
                    {
                        Logger.Warn("[LindenLoginHelper] Can't locate a simulator from custom login URI: " + startLocation);
                    }

                    break;
            }

            if (startRegion.ID != UUID.Zero)
            {
                response.LookAt = startRegion.DefaultLookAt;
                response.RegionX = startRegion.X;
                response.RegionY = startRegion.Y;
                response.SimAddress = startRegion.IP.ToString();
                response.SimPort = (uint)startRegion.Port;

                return true;
            }
            else
            {
                Logger.Error("[LindenLoginHelper] Could not find an available region for login");
                return false;
            }
        }

        public static bool TryGetInventoryInfo(WorldServer server, Avatar avatar, ref LindenLoginData response)
        {
            GetInventorySkeletonMessage message = new GetInventorySkeletonMessage();
            message.Identity = avatar.Identity;
            message.AgentID = avatar.ID;

            Service inventoryService;
            Uri invSkeletonCap;
            Uri invFolderCap;

            if (avatar.Services.TryGetValue(new Uri(CableBeachServices.FILESYSTEM), out inventoryService) &&
                inventoryService.TryGetCapability(new Uri(CableBeachServices.FILESYSTEM_GET_FILESYSTEM_SKELETON), out invSkeletonCap))
            {
                // Fetch the inventory skeleton for this avatar
                CapsClient request = new CapsClient(invSkeletonCap);
                OSDMap responseMap = request.GetResponse(message.Serialize(), OSDFormat.Json, REQUEST_TIMEOUT) as OSDMap;

                if (responseMap != null)
                {
                    GetInventorySkeletonReplyMessage reply = new GetInventorySkeletonReplyMessage();
                    reply.Deserialize(responseMap);

                    response.AgentInventory = new ArrayList(reply.Folders.Length);
                    for (int i = 0; i < reply.Folders.Length; i++)
                    {
                        GetInventorySkeletonReplyMessage.Folder folder = reply.Folders[i];

                        Hashtable folderData = new Hashtable();
                        folderData["name"] = folder.Name;
                        folderData["parent_id"] = folder.ParentID.ToString();
                        folderData["version"] = folder.Version;
                        folderData["type_default"] = (int)CableBeachUtils.ContentTypeToSLAssetType(folder.PreferredContentType);
                        folderData["folder_id"] = folder.FolderID.ToString();

                        if (folder.ParentID == UUID.Zero)
                            response.InventoryRoot = folder.FolderID;

                        response.AgentInventory.Add(folderData);
                    }
                    
                    // TODO: get from config, no hardcoding
                    response.InventoryLibraryOwner = new UUID("11111111-1111-0000-0000-000100bba000");
                    response.InventoryLibRoot = new UUID("00000112-000f-0000-0000-000100bba000");

                    if (inventoryService.TryGetCapability(new Uri(CableBeachServices.FILESYSTEM_GET_FOLDER_CONTENTS), out invFolderCap))
                    {
                        GetFolderContentsMessage worldLibMessage = new GetFolderContentsMessage();
                        worldLibMessage.AgentID = response.InventoryLibraryOwner;
                        worldLibMessage.Identity = null;
                        worldLibMessage.FolderID = response.InventoryLibRoot;

                        CapsClient worldLibRequest = new CapsClient(invFolderCap);
                        OSDMap worldLibResponseMap = request.GetResponse(worldLibMessage.Serialize(), OSDFormat.Json, REQUEST_TIMEOUT) as OSDMap;

                        if (worldLibResponseMap != null)
                        {
                            GetInventorySkeletonReplyMessage worldLibReply = new GetInventorySkeletonReplyMessage();
                            worldLibReply.Deserialize(worldLibResponseMap);

                            response.InventoryLibrary = new ArrayList(worldLibReply.Folders.Length);
                            for (int i = 0; i < worldLibReply.Folders.Length; i++)
                            {
                                GetInventorySkeletonReplyMessage.Folder folder = worldLibReply.Folders[i];

                                Hashtable folderData = new Hashtable();
                                folderData["name"] = folder.Name;
                                folderData["parent_id"] = folder.ParentID.ToString();
                                folderData["version"] = folder.Version;
                                folderData["type_default"] = (int)CableBeachUtils.ContentTypeToSLAssetType(folder.PreferredContentType);
                                folderData["folder_id"] = folder.FolderID.ToString();

                                response.InventoryLibrary.Add(folderData);
                            }
                        }

                    }

                    if (response.InventoryRoot != UUID.Zero)
                    {
                        // TODO: Set the initial outfit for this avatar
                        //response.SetInitialOutfit("", false);

                        return true;
                    }
                }
            }

            Logger.Error("[LindenLoginHelper] Failed to fetch the inventory skeleton for " + avatar.Identity);
            response.AgentInventory = null;
            return false;
        }

        static bool TryPrepareLogin(WorldServer server, Avatar avatar, RegionInfo startRegion, Vector3 startPosition, string clientVersion,
            IPAddress clientIP, ref LindenLoginData response)
        {
            EnableClientMessage message = new EnableClientMessage();
            message.Identity = avatar.Identity;
            message.AgentID = avatar.ID;
            message.Attributes = avatar.Attributes;
            message.CallbackUri = null;
            message.ChildAgent = false;
            message.CircuitCode = CreateCircuitCode();
            message.ClientVersion = clientVersion;
            message.IP = clientIP;
            message.RegionHandle = startRegion.Handle;
            message.SecureSessionID = response.SecureSessionID;
            message.Services = avatar.Services.ToMessageDictionary();
            message.SessionID = response.SessionID;

            Uri enableClientCap;
            if (startRegion.Capabilities.TryGetValue(new Uri(CableBeachServices.SIMULATOR_ENABLE_CLIENT), out enableClientCap))
            {
                CapsClient request = (server.HttpCertificate != null) ?
                new CapsClient(enableClientCap, server.HttpCertificate) :
                new CapsClient(enableClientCap);

                OSDMap responseMap = request.GetResponse(message.Serialize(), OSDFormat.Json, REQUEST_TIMEOUT) as OSDMap;

                if (responseMap != null)
                {
                    EnableClientReplyMessage reply = new EnableClientReplyMessage();
                    reply.Deserialize(responseMap);

                    if (reply.SeedCapability != null)
                    {
                        Logger.Info("enable_client succeeded, sent circuit code " + message.CircuitCode + " and received seed capability " +
                            reply.SeedCapability + " from " + enableClientCap);

                        response.CircuitCode = message.CircuitCode;
                        response.SeedCapability = reply.SeedCapability.ToString();
                        return true;
                    }
                    else
                    {
                        Logger.Error("[LindenLoginHelper] enable_client call to region " + startRegion.Name + " for login from " + avatar.Identity +
                            " failed, did not return a seed capability");
                    }
                }
                else
                {
                    Logger.Error("[LindenLoginHelper] enable_client call to region " + startRegion.Name + " for login from " + avatar.Identity +
                        " failed, could not contact or invalid response");
                }
            }
            else
            {
                Logger.Error("[LindenLoginHelper] enable_client call failed, region " + startRegion.Name +
                    " does not have an enable_client capability");
            }

            return false;
        }

        public static int CreateCircuitCode()
        {
            // TODO: Track these so we don't generate duplicate circuit codes
            return rng.Next(0, Int32.MaxValue);
        }

        static RegionInfo GetNearestRegion(WorldServer server, Avatar avatar, uint regionX, uint regionY)
        {
            RegionInfo region = new RegionInfo();
            server.MapProvider.TryFetchRegionNearest(regionX, regionY, out region);
            return region;
        }

        #region Login XML responses

        public static XmlRpcResponse CreateFailureResponse(string reason, string message, bool loginSuccess)
        {
            Hashtable responseData = new Hashtable(3);
            responseData["reason"] = reason;
            responseData["message"] = message;
            responseData["login"] = loginSuccess.ToString().ToLower();

            XmlRpcResponse response = new XmlRpcResponse();
            response.Value = responseData;
            return response;
        }

        public static XmlRpcResponse CreateLoginFailedResponse()
        {
            return CreateFailureResponse(
                "key",
                "Could not authenticate your avatar. Please check your username and password, and check the grid if problems persist.",
                false);
        }

        public static XmlRpcResponse CreateLoginGridErrorResponse()
        {
            return CreateFailureResponse(
                "key",
                "Error connecting to grid. Could not perceive credentials from login XML.",
                false);
        }

        public static XmlRpcResponse CreateLoginBlockedResponse()
        {
            return CreateFailureResponse(
                "presence",
                "Logins are currently restricted. Please try again later",
                false);
        }

        public static XmlRpcResponse CreateLoginInternalErrorResponse()
        {
            return CreateFailureResponse(
                "key",
                "The login server failed to complete the login process. Please try again later",
                false);
        }

        public static XmlRpcResponse CreateLoginServicesErrorResponse()
        {
            return CreateFailureResponse(
                "key",
                "The login server failed to locate a required grid service. Please try again later",
                false);
        }

        public static XmlRpcResponse CreateLoginNoRegionResponse()
        {
            return CreateFailureResponse(
                "key",
                "The login server could not find an available region to login to. Please try again later",
                false);
        }

        #endregion Login XML responses
    }
}
